#include "grflib_stdafx.h"
#include "../include/ObjLoader.h"
#include <vector>
#include <boost/shared_ptr.hpp>
#include <boost/unordered_map.hpp>
#include <fstream>
#include <Logger.h>
#include "../include/VertexFormat.h"
#include "../include/Material.h"
#include "../include/Texture.h"
#include "../include/D3D10Engine.h"
#include <ImgHelper.h>
#include <boost/algorithm/string.hpp>

namespace grflib { namespace engine { namespace aux { namespace mesh_loader {

struct OBJVERTEX
{
	D3DXVECTOR3 position;
    D3DXVECTOR3 normal;
    D3DXVECTOR2 texcoord;
};


HRESULT LoadObjFile(const TCHAR *filename, grflib::engine::aux::CStaticMesh &mesh, UINT autoMipmapThreshold)
{
    using namespace grflib::engine::aux;
    using namespace grflib::engine::core;
	HRESULT hr;

	//Obtain inside pointers from mesh data structure
	std::size_t *pNGrp = 0;
	std::vector< boost::shared_ptr<std::vector<float>> > *pVertexVec = 0;
	std::vector< boost::shared_ptr<std::vector<DWORD>> > *pIndexVec = 0;
	std::vector<D3D10_PRIMITIVE_TOPOLOGY> *pTopology = 0;
	std::vector< boost::shared_ptr<std::vector<D3D10_INPUT_ELEMENT_DESC>> > *pLayoutVec = 0;
	std::vector<std::size_t> *pStrideVec = 0;
	boost::unordered_map<UINT, std::string> *pMaterialMap = 0;

	mesh.Expose(&pNGrp, &pVertexVec, &pIndexVec, &pTopology, &pLayoutVec, &pStrideVec, &pMaterialMap);
	BBox<float> &refBBox = mesh.GetBoundingBox();
	refBBox.Clear();

	pVertexVec->clear();
	pIndexVec->clear();
	pTopology->clear();
	pLayoutVec->clear();
	pStrideVec->clear();
	pMaterialMap->clear();
    
	//*pNGrp = 1; //only 1 group supported now
	std::size_t currentGroup = 0;
	boost::shared_ptr<std::vector<float>> pVertices(new std::vector<float>);
	boost::shared_ptr<std::vector<DWORD>> pIndices(new std::vector<DWORD>);
	boost::shared_ptr<std::vector<D3D10_INPUT_ELEMENT_DESC>> pElements(new std::vector<D3D10_INPUT_ELEMENT_DESC>);
	char strCurMaterial[MAX_PATH];
	TCHAR szMaterialFilename[MAX_PATH] = {0};
	std::string strMaterialFilename;

    // Create temporary storage for the input data. Once the data has been loaded into
    // a reasonable format we can create a D3DXMesh object and load it with the mesh data.
    std::vector<D3DXVECTOR3> Positions;
    std::vector<D3DXVECTOR3> Normals;
    std::vector<D3DXVECTOR2> TexCoords;
	boost::unordered_map<UINT, UINT> vertexHashTable;
	typedef boost::unordered_map<UINT, UINT>::iterator cache_iter;

    // The first subset uses the default material
    //Material* pMaterial = new Material();
    //if( pMaterial == NULL )
    //    return E_OUTOFMEMORY;

    //InitMaterial( pMaterial );
    //wcscpy_s( pMaterial->strName, MAX_PATH - 1, L"default" );
    //m_Materials.Add( pMaterial );

    DWORD dwCurSubset = 0;

	// File input
	TCHAR strCommand[256] = {0};
	TCHAR wszTmp[MAX_PATH] = {0};

	std::string partialPath, fullPath;

	w32lib::WStrToStr(partialPath, filename);
	V_RETURN(_SearchPaths.Complete(partialPath, fullPath));

	_TIFSTREAM infile( fullPath );
	if( !infile.good() )
	{
		W32ERR(_T("Can't open mesh file!"));
		return E_FAIL;
	}

	while (true)
	{
		infile >> strCommand;
		if( !infile )
			break;

		if( 0 == wcscmp( strCommand, _T("#") ) )
		{
			// Comment
		}
		else if( 0 == wcscmp( strCommand, _T("v") ) )
		{
			// Vertex Position
			float x, y, z;
			infile >> x >> y >> z;
			Positions.push_back( D3DXVECTOR3( x, y, z ) );
		}
		else if( 0 == wcscmp( strCommand, _T("vt") ) )
		{
			// Vertex TexCoord
			float u, v;
			infile >> u >> v;
			TexCoords.push_back( D3DXVECTOR2( u, v ) );
		}
		else if( 0 == wcscmp( strCommand, _T("vn") ) )
		{
			// Vertex Normal
			float x, y, z;
			infile >> x >> y >> z;
			Normals.push_back( D3DXVECTOR3( x, y, z ) );
		}
		else if( 0 == wcscmp( strCommand, _T("f") ) )
		{
			// Face
			UINT iPosition, iTexCoord, iNormal;
			OBJVERTEX vertex;

			for( UINT iFace = 0; iFace < 3; iFace++ )
			{
				ZeroMemory( &vertex, sizeof( OBJVERTEX ) );

				// OBJ format uses 1-based arrays
				infile >> iPosition;
				vertex.position = Positions[ iPosition - 1 ];

				if( '/' == infile.peek() )
				{
					infile.ignore();

					if( '/' != infile.peek() )
					{
						// Optional texture coordinate
						infile >> iTexCoord;
						vertex.texcoord = TexCoords[ iTexCoord - 1 ];
					}

					if( '/' == infile.peek() )
					{
						infile.ignore();

						// Optional vertex normal
						infile >> iNormal;
						vertex.normal = Normals[ iNormal - 1 ];
					}
				}

				// If a duplicate vertex doesn't exist, add this vertex to the Vertices
				// list. Store the index in the Indices array. The Vertices and Indices
				// lists will eventually become the Vertex Buffer and Index Buffer for
				// the mesh.
				cache_iter iter = vertexHashTable.find(iPosition);
                bool bRepeat = false;
				if (iter != vertexHashTable.end()) //found
				{
                    DWORD offset = (iter->second) * 8;
                    static const float fEpsilon = 1e-7f;
                    if (((*pVertices)[offset + 3] - vertex.normal.x < fEpsilon)
                        && ((*pVertices)[offset + 4] - vertex.normal.y < fEpsilon)
                        && ((*pVertices)[offset + 5] - vertex.normal.z < fEpsilon)
                        && ((*pVertices)[offset + 6] - vertex.texcoord.x < fEpsilon)
                        && ((*pVertices)[offset + 7] - vertex.texcoord.y < fEpsilon))
                    {
                        pIndices->push_back(static_cast<std::size_t>(iter->second));
                        bRepeat = true;
                    }
				}
				
                if (!bRepeat)
				{
					pVertices->push_back(vertex.position.x);
					pVertices->push_back(vertex.position.y);
					pVertices->push_back(vertex.position.z);
					pVertices->push_back(vertex.normal.x);
					pVertices->push_back(vertex.normal.y);
					pVertices->push_back(vertex.normal.z);
					pVertices->push_back(vertex.texcoord.x);
					pVertices->push_back(vertex.texcoord.y);
					DWORD index = pVertices->size() / 8 - 1; /* 8 floats per vertex */
					vertexHashTable[iPosition] = index;
					pIndices->push_back(index);

					//bounding calculation
					refBBox.AddMinMax(0, vertex.position.x);
					refBBox.AddMinMax(1, vertex.position.y);
					refBBox.AddMinMax(2, vertex.position.z);
				}

			}
			//m_Attributes.push_back( dwCurSubset );
		}
		else if( 0 == wcscmp( strCommand, _T("mtllib") ) )
		{
			// Material library
			infile >> szMaterialFilename;
			std::string partialFilename;
			w32lib::WStrToStr(partialFilename, szMaterialFilename);
			_SearchPaths.Complete(partialFilename, strMaterialFilename);
			
			V(LoadMtlFile(strMaterialFilename.c_str(), autoMipmapThreshold));
		}
		else if( 0 == wcscmp( strCommand, _T("usemtl") ) )
		{
			++currentGroup;
			
			if (currentGroup > 1) //not first group, flush last group into mesh
			{
				pVertexVec->push_back(pVertices);
				pIndexVec->push_back(pIndices);
				pTopology->push_back(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
				std::size_t stride = 0;
				if (!vertex_format::Mapping(*pElements, stride, 1, 1))
				{
					W32ERR(_T("Unsupported Obj format!"));
					return E_NOTIMPL;
				}
				pLayoutVec->push_back(pElements);
				pStrideVec->push_back(stride);
				(*pMaterialMap)[currentGroup - 2] = (strMaterialFilename + ":" + strCurMaterial);

				//create new containers for the next group of vertices
				pVertices.reset(new std::vector<float>);
				pIndices.reset(new std::vector<DWORD>);
				pElements.reset(new std::vector<D3D10_INPUT_ELEMENT_DESC>);
				vertexHashTable.clear();
			}

			//Read Material Name
			infile >> wszTmp;
#ifdef UNICODE
			w32lib::WCharToChar(strCurMaterial, wszTmp);
#else
			_tcscpy_s(strCurMaterial, wszTmp);
#endif
		}
		else
		{
			// Unimplemented or unrecognized command
		}

		infile.ignore( 1000, '\n' );
	}
	infile.close();

	if (pVertices->size() != 0)
	{
		pVertexVec->push_back(pVertices);
		pIndexVec->push_back(pIndices);
		pTopology->push_back(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
		std::size_t stride = 0;
		if (!vertex_format::Mapping(*pElements, stride, 1, 1))
		{
			W32ERR(_T("Unsupported Obj format!"));
			return E_NOTIMPL;
		}
		pLayoutVec->push_back(pElements);
		pStrideVec->push_back(stride);
		(*pMaterialMap)[currentGroup - 1] = (strMaterialFilename + ":" + strCurMaterial);
	}

	*pNGrp = currentGroup;

	// Cleanup

	return S_OK;
}

HRESULT LoadMtlFile(const char *filename, UINT autoMipmapThreshold)
{
	wchar_t szTmp[MAX_PATH] = {0};
	w32lib::CharToWChar(szTmp, filename);
	return LoadMtlFile(szTmp, autoMipmapThreshold);
}

HRESULT LoadMtlFile(const wchar_t *filename, UINT autoMipmapThreshold)
{
    using namespace grflib::engine::core;
	HRESULT hr;

	//open mtl file
	std::ifstream infile;
	infile.open(filename);
	if (!infile.good())
	{
		W32ERR(_T("Failed to open %s!"), filename);
		return E_FAIL;
	}
	char filenameASC[MAX_PATH] = {0};
#ifdef UNICODE
	w32lib::WCharToChar(filenameASC, filename);
#else
	_tcscpy_s(filenameASC, filename);
#endif

	//temp material
	boost::shared_ptr<CMaterial> pMaterial;
	std::size_t nMaterial = 0;

	//parsing
	char strCommand[MAX_PATH] = {0};
	char strMaterialName[MAX_PATH] = {0};
	std::string strTmp;

	while (true)
	{
		infile >> strCommand;
		_strlwr_s(strCommand);
		if (!infile)
			break;

		if( 0 == strcmp( strCommand, "newmtl" ) )
		{
			++nMaterial;

			if (nMaterial > 1) //not first material, add last one to resource manager
			{
				_ResrcMan.AddObj(strMaterialName, boost::dynamic_pointer_cast<IResource>(pMaterial));
			}

			//read material name
			pMaterial.reset(new CMaterial);
            pMaterial->Reset();
			std::getline(infile, strTmp);
			boost::trim(strTmp);
			sprintf_s(strMaterialName, "%s:%s", filenameASC, strTmp.c_str());

			UINT appendix = 0;
			while (true)
			{
				if (!_ResrcMan.FindObj(strMaterialName)) //name is ok
					break;
				//name already exists, try another appendix
				sprintf_s(strMaterialName, "%s:%s%d", filenameASC, strTmp.c_str(), ++appendix);				
			}
			continue;
		}

		if (!pMaterial) //ignore all commands before a valid "newmtl"
			continue;

		if( 0 == strcmp( strCommand, "#" ) )
		{
			// Comment
			std::getline(infile, strTmp);
			continue;
		}
		else if( 0 == strcmp( strCommand, "ka" ) )
		{
			// Ambient color
			float r, g, b;
			infile >> r >> g >> b;
			pMaterial->ambient[0] = r;
			pMaterial->ambient[1] = g;
			pMaterial->ambient[2] = b;
		}
		else if( 0 == strcmp( strCommand, "kd" ) )
		{
			// Diffuse color
			float r, g, b;
			infile >> r >> g >> b;
			pMaterial->diffuse[0] = r;
			pMaterial->diffuse[1] = g;
			pMaterial->diffuse[2] = b;
		}
		else if( 0 == strcmp( strCommand, "ks" ) )
		{
			// Specular color
			float r, g, b;
			infile >> r >> g >> b;
			pMaterial->specular[0] = r;
			pMaterial->specular[1] = g;
			pMaterial->specular[2] = b;
		}
		else if( 0 == strcmp( strCommand, "d" ) || 0 == strcmp( strCommand, "Tr" ) )
		{
			// Alpha
			infile >> pMaterial->alpha;
		}
		else if( 0 == strcmp( strCommand, "ns" ) )
		{
			// Shininess
			infile >> pMaterial->specularity;
		}
		else if( 0 == strcmp( strCommand, "map_kd" ) 
			    || 0 == strcmp( strCommand, "map_ka" )
				|| 0 == strcmp( strCommand, "map_ks" )
				|| 0 == strcmp( strCommand, "map_bump")
				|| 0 == strcmp( strCommand, "map_opacity")
				|| 0 == strcmp( strCommand, "bump"))
		{
			// Texture
			//infile >> strTmp;
			//infile.get(strTmp, MAX_PATH, '\n');
			std::getline(infile, strTmp);
			boost::trim(strTmp);
			if (strTmp.length() == 0)
				continue;

			//find complete path given filename
			std::string strTextureFile;
			hr = _SearchPaths.Complete(strTmp, strTextureFile);

			if (!SUCCEEDED(hr)) //texture file NOT found
			{
				W32ERR(_T("Texture file not found!"));
			}
			else //texture file exists
			{
				boost::shared_ptr<CTexture2D> pTexture = boost::dynamic_pointer_cast<CTexture2D>(_ResrcMan.FindObj(strTextureFile));
				//check if texture is in memory already
				if (pTexture)
				{
					std::string stdName;
					ObjTextureDescToStd(strCommand, stdName);
					if (stdName.size() != 0)
						pMaterial->textureSlots[stdName] = strTextureFile;
				}
				else
				{
					//load image
#ifdef UNICODE
					std::wstring filenameW;
					w32lib::StrToWStr(filenameW, strTextureFile);
					w32lib::ImagePtr imgPtr = w32lib::image_helper::LoadImageFromDisk(filenameW);
#else
					w32lib::ImagePtr imgPtr = w32lib::image_helper::LoadImageFromDisk(strTextureFile);
#endif
					if (imgPtr.m_ptr.pU8 == 0)
						W32ERR(_T("Failed to load image!"));
					else
					{
						//padding pixels to 32bit if 24bit
						if (24 == imgPtr.GetBPP())
						{
							reinterpret_cast<w32lib::CImgTmpl<unsigned char> *>(imgPtr.Get())->AddChannel(1, 255);
						}

						//create texture from ImagePtr
						pTexture.reset(new CTexture2D(TextureSpec::TU_GPU_READ, TextureSpec::BT_NONE, imgPtr));
						if (!pTexture)
							W32ERR(_T("Failed to create texture object!"));
						else
						{
                            if (max(imgPtr->GetWidth(), imgPtr->GetHeight()) >= autoMipmapThreshold)
                                pTexture->UseAutoMipmap() = true;
							//add texture to _ResrcMan
							_ResrcMan.AddObj(strTextureFile, boost::dynamic_pointer_cast<IResource>(pTexture));

							//add image name to material texture slots
							std::string stdName;
							ObjTextureDescToStd(strCommand, stdName);
							if (stdName.size() != 0)
								pMaterial->textureSlots[stdName] = strTextureFile;
						}
					}
				}
			}
			continue;
		}
		else
		{
			// Unimplemented or unrecognized command
		}

		infile.ignore( 1000, L'\n' );
	}

	infile.close();

	if (pMaterial)
		_ResrcMan.AddObj(strMaterialName, boost::dynamic_pointer_cast<IResource>(pMaterial));

	return S_OK;
}

void ObjTextureDescToStd(const std::string &objDesc, std::string &stdDesc)
{
	if (objDesc == "map_kd")
		stdDesc = "diffuse";
	else if (objDesc == "map_ka")
		stdDesc = "ambient";
	else if (objDesc == "map_ks")
		stdDesc = "specular";
	else if (objDesc == "map_opacity")
		stdDesc = "alpha";
	else if (objDesc == "map_bump" || objDesc == "bump")
		stdDesc = "bump";
	else //unknwon
		stdDesc.clear();
}

}}}} //namespaces